/**
 * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
import { default as TableWalker } from '../tablewalker';
import { createEmptyTableCell, updateNumericAttribute } from './common';
/**
 * Returns a cropped table according to given dimensions.

 * To return a cropped table that starts at first row and first column and end in third row and column:
 *
 * ```ts
 * const croppedTable = cropTableToDimensions( table, {
 *   startRow: 1,
 *   endRow: 3,
 *   startColumn: 1,
 *   endColumn: 3
 * }, writer );
 * ```
 *
 * Calling the code above for the table below:
 *
 *        0   1   2   3   4                      0   1   2
 *      ┌───┬───┬───┬───┬───┐
 *   0  │ a │ b │ c │ d │ e │
 *      ├───┴───┤   ├───┴───┤                  ┌───┬───┬───┐
 *   1  │ f     │   │ g     │                  │   │   │ g │  0
 *      ├───┬───┴───┼───┬───┤   will return:   ├───┴───┼───┤
 *   2  │ h │ i     │ j │ k │                  │ i     │ j │  1
 *      ├───┤       ├───┤   │                  │       ├───┤
 *   3  │ l │       │ m │   │                  │       │ m │  2
 *      ├───┼───┬───┤   ├───┤                  └───────┴───┘
 *   4  │ n │ o │ p │   │ q │
 *      └───┴───┴───┴───┴───┘
 */
export function cropTableToDimensions(sourceTable, cropDimensions, writer) {
    const { startRow, startColumn, endRow, endColumn } = cropDimensions;
    // Create empty table with empty rows equal to crop height.
    const croppedTable = writer.createElement('table');
    const cropHeight = endRow - startRow + 1;
    for (let i = 0; i < cropHeight; i++) {
        writer.insertElement('tableRow', croppedTable, 'end');
    }
    const tableMap = [...new TableWalker(sourceTable, { startRow, endRow, startColumn, endColumn, includeAllSlots: true })];
    // Iterate over source table slots (including empty - spanned - ones).
    for (const { row: sourceRow, column: sourceColumn, cell: tableCell, isAnchor, cellAnchorRow, cellAnchorColumn } of tableMap) {
        // Row index in cropped table.
        const rowInCroppedTable = sourceRow - startRow;
        const row = croppedTable.getChild(rowInCroppedTable);
        // For empty slots: fill the gap with empty table cell.
        if (!isAnchor) {
            // But fill the gap only if the spanning cell is anchored outside cropped area.
            // In the table from method jsdoc those cells are: "c" & "f".
            if (cellAnchorRow < startRow || cellAnchorColumn < startColumn) {
                createEmptyTableCell(writer, writer.createPositionAt(row, 'end'));
            }
        }
        // Otherwise clone the cell with all children and trim if it exceeds cropped area.
        else {
            const tableCellCopy = writer.cloneElement(tableCell);
            writer.append(tableCellCopy, row);
            // Trim table if it exceeds cropped area.
            // In the table from method jsdoc those cells are: "g" & "m".
            trimTableCellIfNeeded(tableCellCopy, sourceRow, sourceColumn, endRow, endColumn, writer);
        }
    }
    // Adjust heading rows & columns in cropped table if crop selection includes headings parts.
    addHeadingsToCroppedTable(croppedTable, sourceTable, startRow, startColumn, writer);
    return croppedTable;
}
/**
 * Returns slot info of cells that starts above and overlaps a given row.
 *
 * In a table below, passing `overlapRow = 3`
 *
 *     ┌───┬───┬───┬───┬───┐
 *  0  │ a │ b │ c │ d │ e │
 *     │   ├───┼───┼───┼───┤
 *  1  │   │ f │ g │ h │ i │
 *     ├───┤   ├───┼───┤   │
 *  2  │ j │   │ k │ l │   │
 *     │   │   │   ├───┼───┤
 *  3  │   │   │   │ m │ n │  <- overlap row to check
 *     ├───┼───┤   │   ├───│
 *  4  │ o │ p │   │   │ q │
 *     └───┴───┴───┴───┴───┘
 *
 * will return slot info for cells: "j", "f", "k".
 *
 * @param table The table to check.
 * @param overlapRow The index of the row to check.
 * @param startRow row to start analysis. Use it when it is known that the cells above that row will not overlap. Default value is 0.
 */
export function getVerticallyOverlappingCells(table, overlapRow, startRow = 0) {
    const cells = [];
    const tableWalker = new TableWalker(table, { startRow, endRow: overlapRow - 1 });
    for (const slotInfo of tableWalker) {
        const { row, cellHeight } = slotInfo;
        const cellEndRow = row + cellHeight - 1;
        if (row < overlapRow && overlapRow <= cellEndRow) {
            cells.push(slotInfo);
        }
    }
    return cells;
}
/**
 * Splits the table cell horizontally.
 *
 * @returns Created table cell, if any were created.
 */
export function splitHorizontally(tableCell, splitRow, writer) {
    const tableRow = tableCell.parent;
    const table = tableRow.parent;
    const rowIndex = tableRow.index;
    const rowspan = parseInt(tableCell.getAttribute('rowspan'));
    const newRowspan = splitRow - rowIndex;
    const newCellAttributes = {};
    const newCellRowSpan = rowspan - newRowspan;
    if (newCellRowSpan > 1) {
        newCellAttributes.rowspan = newCellRowSpan;
    }
    const colspan = parseInt(tableCell.getAttribute('colspan') || '1');
    if (colspan > 1) {
        newCellAttributes.colspan = colspan;
    }
    const startRow = rowIndex;
    const endRow = startRow + newRowspan;
    const tableMap = [...new TableWalker(table, { startRow, endRow, includeAllSlots: true })];
    let newCell = null;
    let columnIndex;
    for (const tableSlot of tableMap) {
        const { row, column, cell } = tableSlot;
        if (cell === tableCell && columnIndex === undefined) {
            columnIndex = column;
        }
        if (columnIndex !== undefined && columnIndex === column && row === endRow) {
            newCell = createEmptyTableCell(writer, tableSlot.getPositionBefore(), newCellAttributes);
        }
    }
    // Update the rowspan attribute after updating table.
    updateNumericAttribute('rowspan', newRowspan, tableCell, writer);
    return newCell;
}
/**
 * Returns slot info of cells that starts before and overlaps a given column.
 *
 * In a table below, passing `overlapColumn = 3`
 *
 *    0   1   2   3   4
 *  ┌───────┬───────┬───┐
 *  │ a     │ b     │ c │
 *  │───┬───┴───────┼───┤
 *  │ d │ e         │ f │
 *  ├───┼───┬───────┴───┤
 *  │ g │ h │ i         │
 *  ├───┼───┼───┬───────┤
 *  │ j │ k │ l │ m     │
 *  ├───┼───┴───┼───┬───┤
 *  │ n │ o     │ p │ q │
 *  └───┴───────┴───┴───┘
 *                ^
 *                Overlap column to check
 *
 * will return slot info for cells: "b", "e", "i".
 *
 * @param table The table to check.
 * @param overlapColumn The index of the column to check.
 */
export function getHorizontallyOverlappingCells(table, overlapColumn) {
    const cellsToSplit = [];
    const tableWalker = new TableWalker(table);
    for (const slotInfo of tableWalker) {
        const { column, cellWidth } = slotInfo;
        const cellEndColumn = column + cellWidth - 1;
        if (column < overlapColumn && overlapColumn <= cellEndColumn) {
            cellsToSplit.push(slotInfo);
        }
    }
    return cellsToSplit;
}
/**
 * Splits the table cell vertically.
 *
 * @param columnIndex The table cell column index.
 * @param splitColumn The index of column to split cell on.
 * @returns Created table cell.
 */
export function splitVertically(tableCell, columnIndex, splitColumn, writer) {
    const colspan = parseInt(tableCell.getAttribute('colspan'));
    const newColspan = splitColumn - columnIndex;
    const newCellAttributes = {};
    const newCellColSpan = colspan - newColspan;
    if (newCellColSpan > 1) {
        newCellAttributes.colspan = newCellColSpan;
    }
    const rowspan = parseInt(tableCell.getAttribute('rowspan') || '1');
    if (rowspan > 1) {
        newCellAttributes.rowspan = rowspan;
    }
    const newCell = createEmptyTableCell(writer, writer.createPositionAfter(tableCell), newCellAttributes);
    // Update the colspan attribute after updating table.
    updateNumericAttribute('colspan', newColspan, tableCell, writer);
    return newCell;
}
/**
 * Adjusts table cell dimensions to not exceed limit row and column.
 *
 * If table cell width (or height) covers a column (or row) that is after a limit column (or row)
 * this method will trim "colspan" (or "rowspan") attribute so the table cell will fit in a defined limits.
 */
export function trimTableCellIfNeeded(tableCell, cellRow, cellColumn, limitRow, limitColumn, writer) {
    const colspan = parseInt(tableCell.getAttribute('colspan') || '1');
    const rowspan = parseInt(tableCell.getAttribute('rowspan') || '1');
    const endColumn = cellColumn + colspan - 1;
    if (endColumn > limitColumn) {
        const trimmedSpan = limitColumn - cellColumn + 1;
        updateNumericAttribute('colspan', trimmedSpan, tableCell, writer, 1);
    }
    const endRow = cellRow + rowspan - 1;
    if (endRow > limitRow) {
        const trimmedSpan = limitRow - cellRow + 1;
        updateNumericAttribute('rowspan', trimmedSpan, tableCell, writer, 1);
    }
}
/**
 * Sets proper heading attributes to a cropped table.
 */
function addHeadingsToCroppedTable(croppedTable, sourceTable, startRow, startColumn, writer) {
    const headingRows = parseInt(sourceTable.getAttribute('headingRows') || '0');
    if (headingRows > 0) {
        const headingRowsInCrop = headingRows - startRow;
        updateNumericAttribute('headingRows', headingRowsInCrop, croppedTable, writer, 0);
    }
    const headingColumns = parseInt(sourceTable.getAttribute('headingColumns') || '0');
    if (headingColumns > 0) {
        const headingColumnsInCrop = headingColumns - startColumn;
        updateNumericAttribute('headingColumns', headingColumnsInCrop, croppedTable, writer, 0);
    }
}
/**
 * Removes columns that have no cells anchored.
 *
 * In table below:
 *
 *     +----+----+----+----+----+----+----+
 *     | 00 | 01      | 03 | 04      | 06 |
 *     +----+----+----+----+         +----+
 *     | 10 | 11      | 13 |         | 16 |
 *     +----+----+----+----+----+----+----+
 *     | 20 | 21      | 23 | 24      | 26 |
 *     +----+----+----+----+----+----+----+
 *                  ^--- empty ---^
 *
 * Will remove columns 2 and 5.
 *
 * **Note:** This is a low-level helper method for clearing invalid model state when doing table modifications.
 * To remove a column from a table use {@link module:table/tableutils~TableUtils#removeColumns `TableUtils.removeColumns()`}.
 *
 * @internal
 * @returns True if removed some columns.
 */
export function removeEmptyColumns(table, tableUtils) {
    const width = tableUtils.getColumns(table);
    const columnsMap = new Array(width).fill(0);
    for (const { column } of new TableWalker(table)) {
        columnsMap[column]++;
    }
    const emptyColumns = columnsMap.reduce((result, cellsCount, column) => {
        return cellsCount ? result : [...result, column];
    }, []);
    if (emptyColumns.length > 0) {
        // Remove only last empty column because it will recurrently trigger removing empty rows.
        const emptyColumn = emptyColumns[emptyColumns.length - 1];
        // @if CK_DEBUG_TABLE // console.log( `Removing empty column: ${ emptyColumn }.` );
        tableUtils.removeColumns(table, { at: emptyColumn });
        return true;
    }
    return false;
}
/**
 * Removes rows that have no cells anchored.
 *
 * In table below:
 *
 *     +----+----+----+
 *     | 00 | 01 | 02 |
 *     +----+----+----+
 *     | 10 | 11 | 12 |
 *     +    +    +    +
 *     |    |    |    | <-- empty
 *     +----+----+----+
 *     | 30 | 31 | 32 |
 *     +----+----+----+
 *     | 40      | 42 |
 *     +         +    +
 *     |         |    | <-- empty
 *     +----+----+----+
 *     | 60 | 61 | 62 |
 *     +----+----+----+
 *
 * Will remove rows 2 and 5.
 *
 * **Note:** This is a low-level helper method for clearing invalid model state when doing table modifications.
 * To remove a row from a table use {@link module:table/tableutils~TableUtils#removeRows `TableUtils.removeRows()`}.
 *
 * @internal
 * @returns True if removed some rows.
 */
export function removeEmptyRows(table, tableUtils) {
    const emptyRows = [];
    const tableRowCount = tableUtils.getRows(table);
    for (let rowIndex = 0; rowIndex < tableRowCount; rowIndex++) {
        const tableRow = table.getChild(rowIndex);
        if (tableRow.isEmpty) {
            emptyRows.push(rowIndex);
        }
    }
    if (emptyRows.length > 0) {
        // Remove only last empty row because it will recurrently trigger removing empty columns.
        const emptyRow = emptyRows[emptyRows.length - 1];
        // @if CK_DEBUG_TABLE // console.log( `Removing empty row: ${ emptyRow }.` );
        tableUtils.removeRows(table, { at: emptyRow });
        return true;
    }
    return false;
}
/**
 * Removes rows and columns that have no cells anchored.
 *
 * In table below:
 *
 *     +----+----+----+----+
 *     | 00      | 02      |
 *     +----+----+         +
 *     | 10      |         |
 *     +----+----+----+----+
 *     | 20      | 22 | 23 |
 *     +         +    +    +
 *     |         |    |    | <-- empty row
 *     +----+----+----+----+
 *             ^--- empty column
 *
 * Will remove row 3 and column 1.
 *
 * **Note:** This is a low-level helper method for clearing invalid model state when doing table modifications.
 * To remove a rows from a table use {@link module:table/tableutils~TableUtils#removeRows `TableUtils.removeRows()`} and
 * {@link module:table/tableutils~TableUtils#removeColumns `TableUtils.removeColumns()`} to remove a column.
 *
 * @internal
 */
export function removeEmptyRowsColumns(table, tableUtils) {
    const removedColumns = removeEmptyColumns(table, tableUtils);
    // If there was some columns removed then cleaning empty rows was already triggered.
    if (!removedColumns) {
        removeEmptyRows(table, tableUtils);
    }
}
/**
 * Returns adjusted last row index if selection covers part of a row with empty slots (spanned by other cells).
 * The `dimensions.lastRow` is equal to last row index but selection might be bigger.
 *
 * This happens *only* on rectangular selection so we analyze a case like this:
 *
 *        +---+---+---+---+
 *      0 | a | b | c | d |
 *        +   +   +---+---+
 *      1 |   | e | f | g |
 *        +   +---+   +---+
 *      2 |   | h |   | i | <- last row, each cell has rowspan = 2,
 *        +   +   +   +   +    so we need to return 3, not 2
 *      3 |   |   |   |   |
 *        +---+---+---+---+
 *
 * @returns Adjusted last row index.
 */
export function adjustLastRowIndex(table, dimensions) {
    const lastRowMap = Array.from(new TableWalker(table, {
        startColumn: dimensions.firstColumn,
        endColumn: dimensions.lastColumn,
        row: dimensions.lastRow
    }));
    const everyCellHasSingleRowspan = lastRowMap.every(({ cellHeight }) => cellHeight === 1);
    // It is a "flat" row, so the last row index is OK.
    if (everyCellHasSingleRowspan) {
        return dimensions.lastRow;
    }
    // Otherwise get any cell's rowspan and adjust the last row index.
    const rowspanAdjustment = lastRowMap[0].cellHeight - 1;
    return dimensions.lastRow + rowspanAdjustment;
}
/**
 * Returns adjusted last column index if selection covers part of a column with empty slots (spanned by other cells).
 * The `dimensions.lastColumn` is equal to last column index but selection might be bigger.
 *
 * This happens *only* on rectangular selection so we analyze a case like this:
 *
 *       0   1   2   3
 *     +---+---+---+---+
 *     | a             |
 *     +---+---+---+---+
 *     | b | c | d     |
 *     +---+---+---+---+
 *     | e     | f     |
 *     +---+---+---+---+
 *     | g | h         |
 *     +---+---+---+---+
 *               ^
 *              last column, each cell has colspan = 2, so we need to return 3, not 2
 *
 * @returns Adjusted last column index.
 */
export function adjustLastColumnIndex(table, dimensions) {
    const lastColumnMap = Array.from(new TableWalker(table, {
        startRow: dimensions.firstRow,
        endRow: dimensions.lastRow,
        column: dimensions.lastColumn
    }));
    const everyCellHasSingleColspan = lastColumnMap.every(({ cellWidth }) => cellWidth === 1);
    // It is a "flat" column, so the last column index is OK.
    if (everyCellHasSingleColspan) {
        return dimensions.lastColumn;
    }
    // Otherwise get any cell's colspan and adjust the last column index.
    const colspanAdjustment = lastColumnMap[0].cellWidth - 1;
    return dimensions.lastColumn + colspanAdjustment;
}
